package org.mspring.platform.mail;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.activation.FileTypeMap;
import javax.mail.AuthenticationFailedException;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.MimeMessage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mail.MailException;
import org.springframework.mail.MailParseException;
import org.springframework.mail.MailPreparationException;
import org.springframework.mail.MailSendException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.ConfigurableMimeFileTypeMap;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMailMessage;
import org.springframework.mail.javamail.MimeMessagePreparator;

/**
 * @author Gao Youbo
 * @since 2012-9-27
 */
public class JavaMailSenderImpl implements JavaMailSender {

    private static final Logger log = LoggerFactory.getLogger(JavaMailSenderImpl.class);

    private static final String HEADER_MESSAGE_ID = "Message-ID";
    private static final String DEFAULT_PROTOCOL = "smtp";

    private MailSenderConfig mailSenderConfig;
    private FileTypeMap defaultFileTypeMap;

    public JavaMailSenderImpl(MailSenderConfig mailSenderConfig) {
        ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
        fileTypeMap.afterPropertiesSet();
        this.defaultFileTypeMap = fileTypeMap;
        this.mailSenderConfig = mailSenderConfig;
    }

    // ---------------------------------------------------------------------
    // Implementation of MailSender
    // ---------------------------------------------------------------------
    @Override
    public void send(SimpleMailMessage simpleMessage) throws MailException {
        send(new SimpleMailMessage[] { simpleMessage });
    }

    @Override
    public void send(SimpleMailMessage[] simpleMessages) throws MailException {
        List<MimeMessage> mimeMessages = new ArrayList<>(simpleMessages.length);
        for (SimpleMailMessage simpleMessage : simpleMessages) {
            MimeMailMessage message = new MimeMailMessage(createMimeMessage());
            simpleMessage.copyTo(message);
            mimeMessages.add(message.getMimeMessage());
        }
        doSend(mimeMessages.toArray(new MimeMessage[mimeMessages.size()]), simpleMessages);
    }

    // ---------------------------------------------------------------------
    // Implementation of JavaMailSender
    // ---------------------------------------------------------------------
    @Override
    public MimeMessage createMimeMessage() {
        return new SmartMimeMessage(getSession(), mailSenderConfig.getEncoding(), defaultFileTypeMap);
    }

    @Override
    public MimeMessage createMimeMessage(InputStream contentStream) throws MailException {
        try {
            return new MimeMessage(getSession(), contentStream);
        } catch (MessagingException ex) {
            throw new MailParseException("Could not parse raw MIME content", ex);
        }
    }

    @Override
    public void send(MimeMessage mimeMessage) throws MailException {
        send(new MimeMessage[] { mimeMessage });
    }

    @Override
    public void send(MimeMessage[] mimeMessages) throws MailException {
        doSend(mimeMessages, null);
    }

    @Override
    public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException {
        send(new MimeMessagePreparator[] { mimeMessagePreparator });
    }

    @Override
    public void send(MimeMessagePreparator[] mimeMessagePreparators) throws MailException {
        try {
            List<MimeMessage> mimeMessages = new ArrayList<>(mimeMessagePreparators.length);
            for (MimeMessagePreparator preparator : mimeMessagePreparators) {
                MimeMessage mimeMessage = createMimeMessage();
                preparator.prepare(mimeMessage);
                mimeMessages.add(mimeMessage);
            }
            send(mimeMessages.toArray(new MimeMessage[mimeMessages.size()]));
        } catch (MailException ex) {
            throw ex;
        } catch (MessagingException ex) {
            throw new MailParseException(ex);
        } catch (IOException ex) {
            throw new MailPreparationException(ex);
        } catch (Exception ex) {
            throw new MailPreparationException(ex);
        }
    }

    protected void doSend(MimeMessage[] mimeMessages, Object[] originalMessages) throws MailException {
        Map<Object, Exception> failedMessages = new LinkedHashMap<>();

        final String host = mailSenderConfig.getHost();
        final String username = mailSenderConfig.getUsername();
        final int port = mailSenderConfig.getPort();
        final String password = mailSenderConfig.getPassword();

        Transport transport = null;
        try {
            transport = getTransport();
            transport.connect(host, port, username, password);
        } catch (AuthenticationFailedException ex) {
            log.error("Can't login mail server. MailAuthenticationException", ex);
            return;
        } catch (MessagingException ex) {
            // Effectively, all messages failed...
            for (int i = 0; i < mimeMessages.length; i++) {
                Object original = (originalMessages != null ? originalMessages[i] : mimeMessages[i]);
                failedMessages.put(original, ex);
            }
            // throw new MailSendException("Mail server connection failed", ex,
            // failedMessages);
            log.error("Mail server connection failed. failedMessages : " + failedMessages, ex);
            return;
        }

        try {
            for (int i = 0; i < mimeMessages.length; i++) {
                MimeMessage mimeMessage = mimeMessages[i];
                try {
                    if (mimeMessage.getSentDate() == null) {
                        mimeMessage.setSentDate(new Date());
                    }
                    String messageId = mimeMessage.getMessageID();
                    mimeMessage.saveChanges();
                    if (messageId != null) {
                        // Preserve explicitly specified message id...
                        mimeMessage.setHeader(HEADER_MESSAGE_ID, messageId);
                    }
                    transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients());
                } catch (MessagingException ex) {
                    Object original = (originalMessages != null ? originalMessages[i] : mimeMessage);
                    failedMessages.put(original, ex);
                }
            }
        } finally {
            try {
                transport.close();
            } catch (MessagingException ex) {
                if (!failedMessages.isEmpty()) {
                    throw new MailSendException("Failed to close server connection after message failures", ex, failedMessages);
                } else {
                    throw new MailSendException("Failed to close server connection after message sending", ex);
                }
            }
        }

        if (!failedMessages.isEmpty()) {
            throw new MailSendException(failedMessages);
        }
    }

    protected synchronized Session getSession() {
        Session session = null;
        if (session == null) {
            session = Session.getInstance(getJavaMailProperties());
        }
        return session;
    }

    protected Transport getTransport() throws NoSuchProviderException {
        Session session = getSession();
        String protocol = null;
        if (protocol == null) {
            protocol = session.getProperty("mail.transport.protocol");
            if (protocol == null) {
                protocol = DEFAULT_PROTOCOL;
            }
        }
        return session.getTransport(protocol);
    }

    protected Properties getJavaMailProperties() {
        Map<String, Object> properties = new HashMap<>();
        properties.put("mail.smtp.timeout", "50000");
        properties.put("mail.smtp.auth", true);
        properties.put("mail.smtp.starttls.enable", true);
        properties.put("mail.smtp.socketFactory.port", 25);
        if (mailSenderConfig.getSSLEnable()) {
            properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        }
        properties.put("mail.smtp.socketFactory.fallback", false);
        Properties javaMailProperties = new Properties();
        if (properties != null && properties.size() > 0) {
            for (Map.Entry<String, Object> entry : properties.entrySet()) {
                javaMailProperties.put(entry.getKey(), entry.getValue());
            }
        }
        return javaMailProperties;
    }

}
